﻿using System;
using System.IO;
using System.Linq;
using System.Xml.Serialization;

using Ionic.Zip;

namespace Bookworm.Core
{
    public sealed class Ebook : IDisposable, IEbookInfo
    {
        private ZipFile _zipFile;
        private EbookInfo _info;
        private readonly EbookTocCollection _tableOfContents = new EbookTocCollection();

        private readonly EbookEntryCollection _entries = new EbookEntryCollection();

        #region Constructors

        public Ebook()
        {
            _info = new EbookInfo();
        }

        public Ebook(string filename)
        {
            if(ZipFile.IsZipFile(filename))
            _zipFile = ZipFile.Read(filename);
            if(_zipFile!=null)
            ValidateAndLoadMetadata();
        }

        public Ebook(Stream stream)
        {
            _zipFile = ZipFile.Read(stream);
            ValidateAndLoadMetadata();
        }

        #endregion

        public void Save()
        {
            InternalSave();
            _zipFile.Save();
        }

        public void SaveAs(string filename)
        {
            if(_zipFile==null)
                _zipFile = new ZipFile();
            InternalSave();
            _zipFile.Save(filename);
        }

        public void SaveAs(Stream stream)
        {
            if (_zipFile == null)
                _zipFile = new ZipFile();
            InternalSave();
            _zipFile.Save(stream);
        }

        private void InternalSave()
        {
            ZipEntry metadataDirectory = _zipFile.AddDirectoryByName("metadata");

            string infoContent = SerializeToXml(_info, info => (EbookInfoXml)info);
            _zipFile.AddFileFromString("info.xml", metadataDirectory.FileName, infoContent);

            string tocContent = SerializeToXml(_tableOfContents, toc => (EbookTocXml)toc);
            _zipFile.AddFileFromString("toc.xml", metadataDirectory.FileName, tocContent);

            ZipEntry contentDirectory = _zipFile.AddDirectoryByName("content");

            foreach (EbookEntry entry in _entries)
            {
                switch (entry.State)
                {
                    case EntryState.Added:
                        //TODO: Check if all inputs (FileName, ZipPath, etc) are valid
                        //TODO: Check if file still exists
                        ZipEntry zipEntry = _zipFile.AddFile(entry.FileName, contentDirectory.FileName + entry.ZipPath);
                        //TODO: Throw exception if entry could not be added
                        break;
                    case EntryState.Removed:
                        //TODO: Check if all inputs (FileName, ZipPath, etc) are valid
                        _zipFile.RemoveEntry(entry.ZipEntry);
                        break;
                    case EntryState.Modified:
                        //TODO:
                        break;
                }
            }
        }

        public EbookTocCollection TableOfContents
        {
            get { return _tableOfContents; }
        }

        public void AddEntry(string filename)
        {
            var entry = new EbookEntry();
            entry.FileName = filename;
            entry.ZipPath = string.Empty;
            entry.State = EntryState.Added;
            _entries.Add(entry);
        }

        #region IEbookInfo Members

        public string Title
        {
            get { return (_info.Title); }
            set { _info.Title = value; }
        }

        public SubtitleCollection Subtitles
        {
            get { return (_info.Subtitles); }
        }

        public string Description
        {
            get { return (_info.Description); }
            set { _info.Description = value; }
        }

        public AuthorCollection Authors
        {
            get { return (_info.Authors); }
        }

        public CategoryCollection Categories
        {
            get { return (_info.Categories); }
        }

        public TagCollection Tags
        {
            get { return (_info.Tags); }
        }

        public LinkCollection Links
        {
            get { return (_info.Links); }
        }

        public ImageCollection Images
        {
            get { return (_info.Images); }
        }

        public Format Format
        {
            get { return (_info.Format); }
            set { _info.Format = value; }
        }

        public string CustomFormat
        {
            get { return (_info.CustomFormat); }
            set { _info.CustomFormat = value; }
        }

        public int Rating
        {
            get { return (_info.Rating); }
            set { _info.Rating = value; }
        }

        public string Isbn10
        {
            get { return (_info.Isbn10); }
            set { _info.Isbn10 = value; }
        }

        public string Isbn13
        {
            get { return (_info.Isbn13); }
            set { _info.Isbn13 = value; }
        }

        public string Publisher
        {
            get { return (_info.Publisher); }
            set { _info.Publisher = value; }
        }

        public DateTime? PublishDate
        {
            get { return (_info.PublishDate); }
            set { _info.PublishDate = value; }
        }

        public string Language
        {
            get { return (_info.Language); }
            set { _info.Language = value; }
        }

        public int? PageCount
        {
            get { return (_info.PageCount); }
            set { _info.PageCount = value; }
        }

        #endregion

        #region IDisposable Members

        public void Dispose()
        {
            if (_zipFile != null)
                _zipFile.Dispose();
        }

        #endregion

        private void ValidateAndLoadMetadata()
        {
            ZipEntry infoEntry = _zipFile["metadata/info.xml"];
            //TODO :throw exception if infoEntry is null
            if (infoEntry != null)
            {
                using (Stream infoStream = infoEntry.OpenReader())
                using (StreamReader infoReader = new StreamReader(infoStream))
                {
                    _info = DeserializeFromXml<EbookInfo, EbookInfoXml>(infoReader.ReadToEnd(), info => (EbookInfo) info);
                }
            }
        }

        private static T DeserializeFromXml<T, TXml>(string xmlContent, Converter<TXml, T> converter)
        {
            var serializer = new XmlSerializer(typeof(TXml));
            using (var stringReader = new StringReader(xmlContent))
            {
                object xmlInstance = serializer.Deserialize(stringReader);
                T instance = converter((TXml)xmlInstance);
                return instance;
            }
        }

        private static string SerializeToXml<T, TXml>(T instance, Converter<T, TXml> converter)
        {
            var serializer = new XmlSerializer(typeof(TXml));
            using (var stringWriter = new StringWriter())
            {
                TXml xmlInstance = converter(instance);
                serializer.Serialize(stringWriter, xmlInstance);
                return stringWriter.ToString();
            }
        }
    }
}